package p2p

import (
	"fmt"
	"io/ioutil"
	"path/filepath"
	"strings"

	"chainmaker.org/chainmaker/common/v2/crypto/asym"
	"chainmaker.org/chainmaker/common/v2/helper"
	"chainmaker.org/chainmaker/common/v2/msgbus"
	protocol "chainmaker.org/chainmaker/protocol/v2"
	libcore "github.com/tokentransfer/interfaces/core"
	libp2p "github.com/tokentransfer/interfaces/p2p"
	"github.com/tokentransfer/node/config"
	"github.com/tokentransfer/node/util"

	"github.com/tokentransfer/consensus/p2p/access"

	"github.com/caivega/glog"
)

const (
	TOPIC_PEER_DISCOVERY = "peer_discovery"
	TOPIC_PEER_DATA      = "peer_data"
	TOPIC_PEER_MESSAGE   = "peer_message"
)

func loadFile(filePath string) ([]byte, error) {
	if len(filePath) > 0 {
		var absFilePath string
		var err error
		if !filepath.IsAbs(filePath) {
			absFilePath, err = filepath.Abs(filePath)
		} else {
			absFilePath = filePath
			err = nil
		}
		if err != nil {
			return nil, err
		}
		fileBytes, err := ioutil.ReadFile(absFilePath)
		if err != nil {
			return nil, err
		}
		return fileBytes, nil
	}
	return nil, util.ErrorOfInvalid("empty", "file path")
}

func GetNetType(c *config.Config) (protocol.NetType, error) {
	var netType protocol.NetType
	// load net type
	provider := c.NetConfig.Provider
	emptyProvider := ""
	glog.Infof("load net provider: %s", provider)
	switch strings.ToLower(provider) {
	case "libp2p", emptyProvider:
		netType = protocol.Libp2p
	case "liquid":
		netType = protocol.Liquid
	default:
		return netType, util.ErrorOfInvalid("unsupported", "net provider")
	}
	return netType, nil
}

func initNet(c *config.Config, readyC chan struct{}) (protocol.Net, error) {
	netType, err := GetNetType(c)
	if err != nil {
		return nil, err
	}

	authType := c.NetConfig.AuthType
	emptyAuthType := ""

	// load tls keys and cert path
	keyPath := c.NetConfig.TLSConfig.PrivKeyFile
	if len(keyPath) > 0 {
		if !filepath.IsAbs(keyPath) {
			keyPath, err = filepath.Abs(keyPath)
			if err != nil {
				return nil, err
			}
		}
		glog.Infof("load net tls key file path: %s", keyPath)
	}

	var certPath string
	var pubKeyMod bool
	switch strings.ToLower(authType) {
	case protocol.PermissionedWithKey, protocol.Public:
		pubKeyMod = true
	case protocol.PermissionedWithCert, protocol.Identity, emptyAuthType:
		pubKeyMod = false
		certPath = c.NetConfig.TLSConfig.CertFile
		if len(certPath) > 0 {
			if !filepath.IsAbs(certPath) {
				certPath, err = filepath.Abs(certPath)
				if err != nil {
					return nil, err
				}
			}
			glog.Infof("load net tls cert file path: %s", certPath)
		}
	default:
		return nil, util.ErrorOfInvalid("wrong", "auth type")
	}

	keyBytes, _ := loadFile(keyPath)
	certBytes, _ := loadFile(certPath)
	//gmtls enc key/cert
	encKeyBytes, _ := loadFile(c.NetConfig.TLSConfig.PrivEncKeyFile)
	encCertBytes, _ := loadFile(c.NetConfig.TLSConfig.CertEncFile)

	listenAddr := c.NetConfig.ListenAddr
	if len(listenAddr) == 0 {
		address := c.GetAddress()
		if address == "localhost" {
			address = "127.0.0.1"
		}
		listenAddr = fmt.Sprintf("/ip4/%s/tcp/%d", address, c.GetPort())
	}
	bootstraps := c.GetBootstraps() // c.NetConfig.Seeds

	// new net
	var netFactory NetFactory
	n, err := netFactory.NewNet(
		netType,
		WithReadySignalC(readyC),
		WithListenAddr(listenAddr),
		WithCrypto(pubKeyMod, keyBytes, certBytes, encKeyBytes, encCertBytes),
		WithPeerStreamPoolSize(c.NetConfig.PeerStreamPoolSize),
		WithMaxPeerCountAllowed(c.NetConfig.MaxPeerCountAllow),
		WithPeerEliminationStrategy(c.NetConfig.PeerEliminationStrategy),
		WithSeeds(bootstraps...),
		WithBlackAddresses(c.NetConfig.BlackList.Addresses...),
		WithBlackNodeIds(c.NetConfig.BlackList.NodeIds...),
		WithMsgCompression(c.NetConfig.DebugConfig.UseNetMsgCompression),
		WithInsecurity(c.NetConfig.DebugConfig.IsNetInsecurity),
		WithStunClient(c.NetConfig.StunClient.ListenAddr,
			c.NetConfig.StunClient.StunServerAddr,
			c.NetConfig.StunClient.NetworkType,
			c.NetConfig.StunClient.Enabled),
		WithStunServer(c.NetConfig.StunServer.Enabled,
			c.NetConfig.StunServer.TwoPublicAddress,
			c.NetConfig.StunServer.OtherStunServerAddr,
			c.NetConfig.StunServer.LocalNotifyAddr,
			c.NetConfig.StunServer.OtherNotifyAddr,
			c.NetConfig.StunServer.ListenAddr1,
			c.NetConfig.StunServer.ListenAddr2,
			c.NetConfig.StunServer.ListenAddr3,
			c.NetConfig.StunServer.ListenAddr4,
			c.NetConfig.StunServer.NetworkType),
		WithHolePunch(c.NetConfig.EnablePunch),
	)
	if err != nil {
		return nil, util.ErrorOfInvalid("new net", err.Error())
	}

	privateKey, err := asym.PrivateKeyFromPEM(keyBytes, nil)
	if err != nil {
		return nil, err
	}
	nodeId, err := helper.CreateLibp2pPeerIdWithPrivateKey(privateKey)
	if err != nil {
		return nil, err
	}
	c.SetNodeId(nodeId)

	// load custom chain trust roots
	for _, chainTrustRoots := range c.NetConfig.CustomChainTrustRoots {
		roots := make([][]byte, 0, len(chainTrustRoots.TrustRoots))
		for _, r := range chainTrustRoots.TrustRoots {
			rootBytes, err2 := ioutil.ReadFile(r.Root)
			if err2 != nil {
				glog.Errorf("load custom chain trust roots failed: %s", err2.Error())
				return nil, err2
			}
			roots = append(roots, rootBytes)
		}
		n.SetChainCustomTrustRoots(chainTrustRoots.ChainId, roots)
		glog.Infof("set custom trust roots for chain[%s] success.", chainTrustRoots.ChainId)
	}

	return n, nil
}

type Network struct {
	c *config.Config
	m map[string]libp2p.MessageCallback

	net protocol.Net
}

func (n *Network) Start(conf libcore.Config, m map[string]libp2p.MessageCallback) error {
	c := conf.(*config.Config)

	readyC := make(chan struct{})

	msgb := msgbus.NewMessageBus()

	_, ac, err := access.InitAc(c, msgb)
	if err != nil {
		return err
	}
	net, err := initNet(c, readyC)
	if err != nil {
		return err
	}
	net.AddAC(c.GetChainId(), ac)
	err = net.Start()
	if err != nil {
		return err
	}
	// err = n.net.InitPubSub(n.config.GetChainId(), 0)
	// if err != nil {
	// 	return err
	// }
	for t, cb := range m {
		err = net.DirectMsgHandle(c.GetChainId(), t, protocol.DirectMsgHandler(cb))
		if err != nil {
			return err
		}
	}
	close(readyC)
	n.c = c
	n.m = m
	n.net = net

	return nil
}

func (n *Network) Stop() error {
	for t := range n.m {
		if err := n.net.CancelDirectMsgHandle(n.c.GetChainId(), t); err != nil {
			return err
		}
	}
	if err := n.net.Stop(); err != nil {
		return err
	}
	return nil
}

func (n *Network) SendMessage(chainId string, node string, msgFlag string, netMsg []byte) error {
	err := n.net.SendMsg(chainId, node, msgFlag, netMsg)
	if err != nil {
		return err
	}
	return nil
}

func (n *Network) ListPeers(chainId string) ([]string, error) {
	peers, err := n.net.ChainNodesInfo(n.c.GetChainId())
	if err != nil {
		return nil, err
	}

	l := len(peers)
	ids := make([]string, l)
	for i := 0; i < l; i++ {
		p := peers[i]
		ids[i] = p.NodeUid
	}
	return ids, nil
}

func (n *Network) VerifyPeers(chainId string) error {
	n.net.ReVerifyPeers(n.c.GetChainId())
	return nil
}

// func ReVerifyTrustRoots(c *config.Config, n protocol.Net) error {
// 	netType, err := GetNetType(c)
// 	if err != nil {
// 		return err
// 	}
// 	switch netType {
// 	case protocol.Libp2p:
// 		n, _ := n.(*libp2p.LibP2pNet)

// 	case protocol.Liquid:
// 		n, _ := n.(*liquid.LiquidNet)

// 	}
// 	return nil
// }
